Conversation
152cc26 to
2cca48e
Compare
2f3c1d7 to
78a2a89
Compare
97764a1 to
6d5681c
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces a new plugin-based customization system for snapshot generation, refactors much of the internal adapter/codegen pipeline to use Builder/Custom* abstractions, and extends external storage and dirty-equals support, along with comprehensive tests and documentation updates.
Changes:
- Add a pluggy-based plugin API (
inline_snapshot.plugin) with a@customizehook, default plugin implementations for core types (dataclasses, attrs, pydantic, dirty-equals, paths, externals), and automatic discovery fromconftest.pyand entry points. - Replace the old adapter/value-repr pipeline with a new
Custom/Builder-based system, includingNewAdapter,CustomCode,CustomCall,CustomDict,CustomSequence,CustomExternal, andCustomUndefined/CustomUnmanaged, and wire it through_inline_snapshot,_snapshot/*,_get_snapshot_value,_global_state, and_change. - Rework external snapshot handling (hash/UUID storage,
outsource,external, import insertion) and add broad test coverage for customization, externals, namedtuple/dataclass behavior, docs rendering, and dirty-equals semantics.
Reviewed changes
Copilot reviewed 81 out of 83 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_pytest_plugin.py | Extends config tests to cover default-storage="hash" in pyproject.toml scenarios, validating new default storage behavior. |
| tests/test_preserve_values.py | Adjusts a snapshot to use an expression (3 + 3) ensuring the new comparison/customization logic still preserves original source forms where expected. |
| tests/test_formatting.py | Updates expected formatted snapshots to use canonical list formatting (consistent commas/spaces) under the new formatting/token handling. |
| tests/test_docs.py | Adapts doc mapping tests to treat block_options as a dict, adds isort-based import normalization and align-based hl_lines computation to match new docs processing behavior. |
| tests/test_dirty_equals.py | Adds tests around IsNow handling (removing approx and supporting custom delta) under the new dirty-equals customization. |
| tests/test_customize.py | New test suite validating @customize and Builder behavior (dirty-equals, imports, global lookup, __file__, datetime types, error cases, etc.). |
| tests/test_code_repr.py | Reworks enum/path/HasRepr tests to run through the new Example/inline execution flow and new path/customization behavior. |
| tests/test_builder.py | New tests for Builder.with_default rejecting custom values (ensuring UsageError for invalid defaults). |
| tests/external/test_module_name.py | New tests that exercise module_name_of and import behavior when snapshots live in __init__.py inside a package. |
| tests/external/test_external.py | Updates many external-storage tests for the new hash/uuid behavior, removal of legacy hash storage expectations, new collision and not-found handling, and import insertion signature changes. |
| tests/external/storage/test_hash.py | Adds a test ensuring repeated lookups of the same hash produce stable external references under the new interface. |
| tests/conftest.py | Extends the Example testing environment to use importlib for module execution, deterministic UUIDs for externals, and the new plugin/customization model for freezegun’s FakeDatetime/FakeDate. |
| tests/adapter/test_namedtuple.py | New tests that verify the new namedtuple handling (defaults, positional args, typing.NamedTuple, nested/mixed args) via the new adapter/custom pipeline. |
| tests/adapter/test_general.py | Adds a test ensuring unmanaged values now raise UsageError under the new customization/unmanaged handling. |
| tests/adapter/test_dataclass.py | Adds/adjusts dataclass tests (extra args, positional handling, plugin-based L adapter, custom __init__ semantics) for the new builder/custom pipeline. |
| tests/adapter/test_change_types.py | Fixes a bug in the test helper (code_repr(v) now uses the v parameter instead of an undefined a), aligning with new adapter behavior. |
| src/inline_snapshot/testing/_example.py | Major refactor of the Example runner: switch to importlib-based loading, maintain sys.path, explicitly load conftest.py and register customize hooks via SnapshotSession.register_customize_hooks_from_module, and restore sys.modules/sys.path after runs. |
| src/inline_snapshot/pytest_plugin.py | Extends the pytest plugin to register @customize hooks from already-loaded and newly-registered conftest.py plugins, and maintains the unwrap-based assertrepr compare behavior under the new pipeline. |
| src/inline_snapshot/plugin/_spec.py | Defines the pluggy hook spec/impl markers and the InlineSnapshotPluginSpec.customize hook (with Builder, local/global vars) plus a convenience @customize decorator. |
| src/inline_snapshot/plugin/_default_plugin.py | Implements the default plugin(s) covering lists/tuples/dicts, strings, Counter, functions/types, datetime/timedelta, Path/PurePath, sets/frozensets, Enums/Flags, dataclasses, namedtuples, defaultdict, unmanaged/undefined, Outsourced, and optional attrs/pydantic/dirty-equals behavior. |
| src/inline_snapshot/plugin/init.py | Public plugin API surface re-exporting InlineSnapshotPluginSpec, customize, hookimpl, Builder, Custom, Import, and ImportFrom. |
| src/inline_snapshot/fix_pytest_diff.py | Removes the Unmanaged pretty-printer adapter, now that unmanaged values are represented via CustomUnmanaged. |
| src/inline_snapshot/extra.py | Updates embedded docs/examples to new import ordering and highlight line numbers, in line with the new formatting and hl_lines behavior. |
| src/inline_snapshot/_utils.py | Replaces the old tokenization/value-to-token helpers with a new clone function (now deep-copy based with UsageError on unequal copies, using real_repr), and tweaks string token equality to mark an uncovered branch as pragma: no cover. |
| src/inline_snapshot/_unmanaged.py | Simplifies dirty-equals type detection (__mro__ based), and removes the Unmanaged wrapper and map_unmanaged in favor of CustomUnmanaged. |
| src/inline_snapshot/_types.py | Minor doc/example import reordering (Snapshot before snapshot) to match formatting rules. |
| src/inline_snapshot/_source_file.py | Reintroduces tokenization logic here (_token_of_code), adds format_expression helper, and a code_changed method that compares tokenized expressions to detect meaningful code changes. |
| src/inline_snapshot/_snapshot_session.py | Adds plugin registration (register_customize_hooks_from_module using a small registry and SimpleNamespace) and adjusts storage-dir handling to respect configured storage_dir/default-storage. |
| src/inline_snapshot/_snapshot/undecided_value.py | Completely rewrites UndecidedValue to work in terms of new Custom objects, including an AstToCustom converter for AST→Custom mapping and delegation to the new adapter pipeline. |
| src/inline_snapshot/_snapshot/min_max_value.py | Refactors min/max snapshot behavior to operate on Custom values, computing new code via _code_repr and using SourceFile.code_changed instead of hand-rolled token comparison. |
| src/inline_snapshot/_snapshot/generic_value.py | Central refactor: GenericValue now operates on Custom values, provides to_custom/value_to_custom (using Builder and mock_repr), re-evaluation via reeval, and updated type-error/operation dispatch semantics. |
| src/inline_snapshot/_snapshot/eq_value.py | Equality snapshots now use NewAdapter.compare over Custom values, capturing changes via a generator (split_gen) and computing new code via _code_repr. |
| src/inline_snapshot/_snapshot/dict_value.py | Dict snapshots are reimplemented on top of CustomDict and UndecidedValue, with custom key/value code generation and DictInsert that now carries both code and value tuples. |
| src/inline_snapshot/_snapshot/collection_value.py | List/tuple/collection snapshots now rely on CustomList and SourceFile.code_changed, emitting ListInsert and Replace operations based on new _code_repr output. |
| src/inline_snapshot/_snapshot/init.py | (Present but unchanged content in diff; serves as package marker for snapshot modules under the new architecture.) |
| src/inline_snapshot/_new_adapter.py | New core adapter: implements warn_star_expression, reeval across Custom* types, and NewAdapter.compare for CustomCode, CustomSequence, CustomDict, and CustomCall, emitting ChangeBase instances (Delete/ListInsert/DictInsert/CallArg/Replace). |
| src/inline_snapshot/_inline_snapshot.py | Wires the new AdapterContext, CustomUndefined, and generator-based _new_code/_changes into the snapshot call machinery, including flagging create changes via with_flag. |
| src/inline_snapshot/_global_state.py | Introduces a per-context pluggy PluginManager (pm), seeds it with default plugins and setuptools entry points in enter_snapshot_context, and ensures a fresh plugin environment per snapshot context. |
| src/inline_snapshot/_get_snapshot_value.py | Reimplements unwrap logic to operate via GenericValue._visible_value()._map, treats Outsourced specially, and uses StorageLookupError.files to distinguish lookup failures when unwrapping External/ExternalFile. |
| src/inline_snapshot/_generator_utils.py | New helpers for working with change generators: split_gen, only_value, gen_map, with_flag, and make_gen_map. |
| src/inline_snapshot/_external/_storage/_uuid.py | Updates UuidStorage to raise StorageLookupError(location, files=[]) on lookup failure, aligning with the new error interface. |
| src/inline_snapshot/_external/_storage/_protocol.py | Extends StorageLookupError to carry a files attribute (list of candidate files) while still subclassing Exception. |
| src/inline_snapshot/_external/_storage/_hash.py | Adapts hash storage to pass files into StorageLookupError for collisions/not-found, enabling richer error handling in callers. |
| src/inline_snapshot/_external/_storage/init.py | Returns a storage dict with both "hash" and "uuid" storages for a given storage_dir. |
| src/inline_snapshot/_external/_outsource.py | Redefines Outsourced as a dataclass carrying data, optional suffix, and optional storage, and changes outsource() to validate formats and return Outsourced without immediate storage writes. |
| src/inline_snapshot/_external/_find_external.py | Enhances external detection and import insertion: adds module_name_of, extends ensure_import to handle both from and import statements, obey module docstrings/future imports, and skip self/builtins imports; also introduces an unconditional print() (see below). |
| src/inline_snapshot/_external/_external_location.py | Switches to using the new AdapterContext type, aligning external-location evaluation with the new adapter pipeline. |
| src/inline_snapshot/_external/_external_base.py | Adjusts ExternalBase.__eq__ to understand Outsourced and to react to StorageLookupError(files=...), auto-filling missing externals in fix mode while preserving error signaling when appropriate. |
| src/inline_snapshot/_external/_external.py | Declares _original_location/_location types explicitly and uses AdapterContext for the external helper, integrating with the new comparison pipeline. |
| src/inline_snapshot/_customize/_custom_unmanaged.py | Introduces CustomUnmanaged as the new representation of unmanaged values within the Custom pipeline. |
| src/inline_snapshot/_customize/_custom_undefined.py | Introduces CustomUndefined as the Custom-level sentinel for undefined snapshot parts, with a code representation of "...". |
| src/inline_snapshot/_customize/_custom_sequence.py | Implements CustomSequence plus CustomList/CustomTuple with brace/trailing-comma semantics and per-element _code_repr. |
| src/inline_snapshot/_customize/_custom_external.py | Implements CustomExternal that emits ExternalChange and RequiredImport changes and generates external("...") code using the configured storage and format handlers. |
| src/inline_snapshot/_customize/_custom_dict.py | Implements CustomDict with dict-based _map and _code_repr supporting per-key/value Custom conversion. |
| src/inline_snapshot/_customize/_custom_code.py | Implements CustomCode, Import, ImportFrom, and _simplify_module_path, handling cloning, representation fallback via HasRepr, and emitting RequiredImport changes for needed imports. |
| src/inline_snapshot/_customize/_custom_call.py | Implements CustomCall/CustomDefault, argument lookup, argument defaulting, and call code generation while keeping defaults out of generated code where appropriate. |
| src/inline_snapshot/_customize/_custom.py | Base Custom ABC defining _map, _code_repr, _eval, and node_type/original_value semantics for all custom nodes. |
| src/inline_snapshot/_customize/_builder.py | Core Builder implementation for plugins: provides create_list/tuple/dict/call/code/external, with_default, and access to local/global/import variables, and orchestrates the pm.hook.customize pipeline and mismatch checking. |
| src/inline_snapshot/_customize/init.py | Package marker for the new customization subsystem. |
| src/inline_snapshot/_compare_context.py | Makes compare_context robust with a try/finally to restore the global flag on exceptions. |
| src/inline_snapshot/_code_repr.py | Refactors repr handling to use mock_repr and the new Custom pipeline, deprecates @customize_repr in favor of @customize, and removes the old built-in type-specific repr hooks (now handled by the plugin system). |
| src/inline_snapshot/_change.py | Introduces RequiredImport and integrates import collection with ensure_import, normalizes new/replace code via SourceFile.format_expression, and generalizes list/dict/call-arg insertions to work with the new Custom pipeline. |
| src/inline_snapshot/_adapter_context.py | New small data structures (FrameContext, AdapterContext) encapsulating the current file/frame/qualname and providing an eval helper for AST expressions. |
| src/inline_snapshot/_adapter/value_adapter.py | (Removed legacy adapter implementation; now superseded by NewAdapter/Custom pipeline.) |
| src/inline_snapshot/_adapter/sequence_adapter.py | (Removed legacy list/tuple adapter in favor of CustomSequence and NewAdapter.) |
| src/inline_snapshot/_adapter/generic_call_adapter.py | (Removed legacy generic-call adapter; call handling now lives in CustomCall/NewAdapter.) |
| src/inline_snapshot/_adapter/dict_adapter.py | (Removed legacy dict adapter in favor of CustomDict/NewAdapter.) |
| src/inline_snapshot/_adapter/adapter.py | (Removed legacy adapter core; responsibilities are now split between AdapterContext, NewAdapter, and Builder/Customs.) |
| src/inline_snapshot/_adapter/init.py | (Emptied legacy adapter export; new customization is via inline_snapshot.plugin.) |
| pyproject.toml | Adds typing-extensions and isort, adjusts coverage (ignore_errors, pattern for "..."), makes docs env scripts forward args, and simplifies Hatch env dependencies to use dependency-groups. |
| mkdocs.yml | Adds a new “Plugin” page to the docs nav. |
| docs/testing.md | Reorders imports in testing examples to conform to new style/formatting. |
| docs/plugin.md | New documentation page describing the plugin architecture, @customize hook, Builder API, and several practical examples. |
| docs/external/register_format.md | Minor example import reorder to match new style. |
| docs/external/outsource.md | Updates outsource docs to reflect UUID-based external references instead of hash-based names. |
| docs/external/external.md | Reorders imports in examples and aligns behavior with new external handling. |
| docs/eq_snapshot.md | Updates dirty-equals examples to use IsNow and tweaks surrounding narrative and imports accordingly. |
| docs/customize_repr.md | Marks @customize_repr as deprecated in favor of @customize, with guidance and updated examples. |
| docs/code_generation.md | Minor import order updates in code generation docs. |
| docs/categories.md | Clarifies the update category (why it is hidden by default, and relation to #177) and fixes a small grammar issue. |
| changelog.d/20251201_200505_15r10nk-git_customize.md | Notes new Path/PurePath snapshot behavior (avoiding platform-specific PosixPath/WindowsPath snapshots). |
| README.md | Updates README examples for new import order and adds outcome-errors in the inline-snapshot directive. |
| .github/workflows/ci.yml | Tightens coverage reporting (skip-covered/skip-empty, branch coverage) and stores a textual coverage report in htmlcov/report.txt. |
Comments suppressed due to low confidence (1)
src/inline_snapshot/_snapshot/min_max_value.py:13
- The class 'MinMaxValue' does not override 'eq', but adds the new attribute _new_value.
The class 'MinMaxValue' does not override 'eq', but adds the new attribute _new_value.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
status update:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 83 out of 85 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
src/inline_snapshot/_snapshot/min_max_value.py:13
- The class 'MinMaxValue' does not override 'eq', but adds the new attribute _new_value.
The class 'MinMaxValue' does not override 'eq', but adds the new attribute _new_value.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Hey, thanks for providing an update. Do you think it's usable in prod or should I wait a few more weeks? :) |
|
It's ready to go. I've fixed all the issues I found while testing with pydantic-ai and logfire. |
#177
test with:
or like this if you want to use the latest branch:
@customizeallows you to teach inline-snapshot how you want to create your snapshots.mini howto:
You can also have multiple
@customizehandler.You will mostly create custom call expressions like this: